#!/usr/bin/python3
# -*- coding:utf-8 -*-
# @auth:  spacelacc
# @date:  2025/04/14
# @func:  Custom tableviews for 'data confirm', 'data shown'
# @vers:  V1


import os
from PySide6.QtGui     import QPainter, QPen, QColor
from PySide6.QtCore    import Signal
from PySide6.QtCore    import Qt, QModelIndex, QPoint
from PySide6.QtCore    import QAbstractTableModel
from PySide6.QtWidgets import QTableView, QStyledItemDelegate
 
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
from PySide6.QtWidgets import QApplication, QWidget, QFrame


# Model for checking for probe data parsed from spreadsheet or database
class CustomCounterTableModel(QAbstractTableModel):
    def __init__(self, data=[]):
        super().__init__()
        self._data = data
        self.headers = [
            "ID", "Serial", "Select", "Source", "source",
            "Param1", "Param2", "Param3", "Param4", "Param5",
            "Param6", "Param7", "Param8",
        ]

    def rowCount(self, parent=QModelIndex()):
        if not self._data:
            return 0
        return len(self._data)

    def columnCount(self, parent=QModelIndex()):
        if not self._data:
            return 0
        if not self._data[0]:
            return 0
        return len(self._data[0])

    def headerData(self, section, orient, role):
        if section < 0 or section >= len(self.headers):
            return None
        return self.headers[section]

    def data(self, index, role=Qt.DisplayRole):
        row = index.row()
        col = index.column()
        if role == Qt.DisplayRole:
            # if get float value, format it
            if isinstance(self._data[row][col], float):
                return '{:.5f}'.format(self._data[row][col])

            # 1st column, ID, in actual data's index: 0
            if col == 0:
                return self._data[row][0]
            # 2nd column, Serial Number, in actual data's index: 1
#           elif col == 1:
#               return self._data[row][1]
            # 3rd column, Selection, in actual data's index: 2
            elif col == 2:
                return self._data[row][2]
            # 4th column, Source Kind, in actual data's index: 3
            elif col == 3:    # may be shown by iamge, afterwards
                return self._data[row][3]
            # 5th column, Source Addr, in actual data's index: 4
            elif col == 4:
                return os.path.basename(self._data[row][col])
            else:
                return str(self._data[row][col])
        elif role == Qt.CheckStateRole:
            if self._data[row][2]:
                return Qt.Checked
            else:
                return Qt.Unchecked
        elif role == Qt.TextAlignmentRole:
            return Qt.AlignVCenter
        else:
            return None

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        if role == Qt.EditRole and index.column() == 2:
            self._data[index.row()][2] = value
            self.dataChanged.emit(index, index)
        return True

    # Temporarily done, needs to be refined afterwards
    def contains(self, target:list):
        for d in self._data:
            if target[1] == d[1]:  # same 'serial'
                return True
        return False

    def table_model_data_reset(self):
        self.beginResetModel()
        self._data.clear()
        self.endResetModel()

    def table_model_data_renew(self, data:list):
        self.beginResetModel()
        self._data.clear()
        self._data.extend(data)
        self.endResetModel()

    def table_model_data_update(self, data:list):
        if not data:
            return
        purified = []
        for d in data:
            if not self.contains(d):
                purified.append(d)
        ncnts = len(self._data)
        nrows = len(purified)
        self.beginInsertRows(QModelIndex(), ncnts, ncnts+nrows-1)
        self._data.extend(purified)
        countstart = ncnts
        # update ID for newly updated data
        for i in range(ncnts, ncnts+nrows):
            self._data[i][0] = countstart
            countstart += 1
        self.endInsertRows()


class CustomCounterTableDelegate(QStyledItemDelegate):
    def __init__(self):
        super().__init__()
        self.linehover = -1
        self.chosen = []
        self.colorHover = QColor(0, 143, 191)
        self.colorClick = QColor(255, 110, 170)
        self.colorBase  = QColor(240, 240, 240)

    def onMouseHover(self, line):
        self.linehover = line

    def onMouseLeave(self):
        self.linehover = -1

    def onMouseClick(self, lines):
        self.chosen.clear()
        for line in lines:
            self.chosen.append(line)

    def clearConditions(self):
        self.chosen.clear()
        self.linehover = -1

    def paint(self, painter, option, index):
        painter.save()

        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)

#       if option.state & QStyle.State_Selected:
#           painter.fillRect(option.rect, option.palette.highlight())
#       elif option.state & QStyle.State_MouseOver:
#           painter.fillRect(option.rect, option.palette.highlight())
#       else:
#           painter.fillRect(option.rect, option.palette.base())

        painter.setPen(QPen(Qt.black, 2))
        # 1. Do 'Mouse Hover' background color processing
        if index.row() == self.linehover:
            painter.fillRect(option.rect, self.colorHover)
        else:
            painter.fillRect(option.rect, self.colorBase)

        # 2. Do 'Mouse Click Select' background/foreground color processing
        if self.chosen and index.row() in self.chosen:
            painter.fillRect(option.rect, self.colorClick)

        # 3. Do regular 'Contents Render'
        # draw checkbox for 1st column
        if index.column() == 2:
            w = option.rect.width()
            h = option.rect.height()
            l = option.rect.height() / 4
            bx = option.rect.x()
            by = option.rect.y()
            ox = bx + 0.5*w - 0.5*l
            oy = by + 0.5*h - 0.5*l
            pt1 = QPoint(ox, oy+l/3)
            pt2 = QPoint(ox+l/3, oy+l)
            pt3 = QPoint(ox+l, oy)
            
            data = index.data(Qt.CheckStateRole)
            if data == Qt.Checked:
                painter.drawRect(bx+0.5*w-0.25*h, by+0.5*h-0.25*h, 0.5*h, 0.5*h)
                painter.drawPolyline([pt1, pt2, pt3])
            elif data == Qt.Unchecked:
                painter.drawRect(bx+0.5*w-0.25*h, by+0.5*h-0.25*h, 0.5*h, 0.5*h)

        # draw source kind icon
        elif index.column() == 3:
            w = option.rect.width()
            h = option.rect.height()
            cw = w / 2.0
            ch = h / 2.0
            ww = 0.15*w
            wh = 0.70*h
            bx = option.rect.x()
            by = option.rect.y()

            if index.data() == 0:    #Data comes from spreadsheet
                pt1 = QPoint(bx+cw-0.5*ww, by+ch-0.5*wh)
                pt2 = QPoint(bx+cw-0.5*ww, by+ch+0.5*wh)
                pt3 = QPoint(bx+cw+0.5*ww, by+ch+0.5*wh)
                pt4 = QPoint(bx+cw+0.5*ww, by+ch-0.2*wh)
                pt5 = QPoint(bx+cw+0.1*ww, by+ch-0.5*wh)
                painter.drawPolygon([pt1, pt2, pt3, pt4, pt5])
                painter.drawLine(bx+cw+0.1*ww, by+ch-wh/2, bx+cw+0.1*ww, by+ch-0.2*wh)
                painter.drawLine(bx+cw+0.1*ww, by+ch-0.2*wh, bx+cw+0.5*ww, by+ch-0.2*wh)
                painter.drawLine(bx+cw-0.3*ww, by+ch+0.01*wh, bx+cw+0.3*ww, by+ch+0.01*wh)
                painter.drawLine(bx+cw-0.3*ww, by+ch+0.15*wh, bx+cw+0.3*ww, by+ch+0.15*wh)
                painter.drawLine(bx+cw-0.3*ww, by+ch+0.30*wh, bx+cw+0.3*ww, by+ch+0.30*wh)

        # draw text directly for other columns
        else:
            value = str(index.data(Qt.DisplayRole))
            painter.drawText(option.rect, Qt.AlignCenter, value)
        painter.restore()



class CustomCounterTableView(QTableView):
    # Defines signals here
    signalMouseHover = Signal(int)
    signalMouseLeave = Signal()
    signalMouseClick = Signal(list)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.chosen = []
        self.recorder = -1
        self.previous = -1
        self.modifier = Qt.NoModifier
        self.tabledata = CustomCounterTableModel()
        self.delegate  = CustomCounterTableDelegate()

        self.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.setSelectionBehavior(QTableView.SelectRows)
        self.horizontalHeader().setVisible(False)
        self.verticalHeader().setVisible(False)
        self.resizeRowsToContents()
        self.resizeColumnsToContents()
        self.setFrameShape(QFrame.NoFrame)
        self.setGridStyle(Qt.SolidLine)
        self.setShowGrid(True)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
#       self.customContextMenuRequested.connect(self.popupContext)
        self.setModel(self.tabledata)
        self.setItemDelegate(self.delegate)
        self.setMouseTracking(True)


        # Connecting signals
        self.signalMouseHover.connect(self.delegate.onMouseHover)
        self.signalMouseLeave.connect(self.delegate.onMouseLeave)
        self.signalMouseClick.connect(self.delegate.onMouseClick)

    def leaveEvent(self, event):
        self.signalMouseLeave.emit()
        start = self.model().index(0, 0)
        stop = self.model().index(self.model().rowCount()-1, self.model().columnCount()-1)
        self.model().dataChanged.emit(start, stop)

    def keyPressEvent(self, event):
        self.modifier = event.modifiers()
        # Catch keys for 'selection operation'
        if event.key() == Qt.Key_Space or event.key() == Qt.Key_Escape:
            # Multiple lines have been clicked chosen
            if self.chosen:
                for line in self.chosen:
                    index = self.model().index(line, 2)
                    if event.key() == Qt.Key_Space:
                        index.model().setData(index, not index.data(), Qt.EditRole)
                    else:
                        index.model().setData(index, False, Qt.EditRole)
            else:
                index = self.model().index(self.recorder, 2)
            # ???? Here we can see whether it's possible to
            # combine this and up together afterwards
                if event.key() == Qt.Key_Space:
                    index.model().setData(index, not index.data(), Qt.EditRole)
                else:
                    index.model().setData(index, False, Qt.EditRole)

        # Catch 'arrow keys' to move upward or downward
        elif event.key() == Qt.Key_Up or event.key() == Qt.Key_Down:
            # Validate 'self.previous'
            if self.previous < self.model().rowCount() - 1 and event.key() == Qt.Key_Down:
                self.previous += 1
            if self.previous > 0 and event.key() == Qt.Key_Up:
                self.previous -= 1
            
            self.chosen.clear()
            self.chosen.append(self.previous)
            self.signalMouseClick.emit(self.chosen)

            # Highlight the whole line
            start = self.model().index(self.previous, 0)
            stop  = self.model().index(self.previous, self.model().columnCount()-1)
            self.model().dataChanged.emit(start, stop)

        # Catch othewise keys here
        super().keyPressEvent(event)

    def mouseMoveEvent(self, event):
        hover = self.indexAt(event.position().toPoint())
        self.signalMouseHover.emit(hover.row())

        lineprev = hover.row()-1
        linenext = hover.row()+1
        if lineprev < 0:
            lineprev = 0
        if linenext == self.model().rowCount():
            linenext = self.model().rowCount()-1
        # Notify 'delegate' to update the whole hover line and its neighbors.
        start = self.model().index(lineprev, 0)
        stop = self.model().index(linenext, self.model().columnCount()-1)
        self.model().dataChanged.emit(start, stop)

    def mousePressEvent(self, event):
        self.modifier = event.modifiers()

        # Mouse Left Button Click
        if event.buttons() == Qt.MouseButton.LeftButton:
            clicked = self.indexAt(event.position().toPoint())
            # Not valid click
            if clicked.row() == -1:
                return

            model = clicked.model()
            columns = model.columnCount()-1
            
            # Record current clicked line as 'previous clicked line'
            # Once clicked, update 'previous', but keep 'record' 
            self.previous = clicked.row()  
            # Qt.NoModifier
            if self.modifier == Qt.NoModifier:
                self.chosen.clear()
                self.recorder = clicked.row()
                self.previous = clicked.row()
                if clicked.isValid() and clicked.column() == 2:
#                   model.setData(clicked, not clicked.data(), Qt.EditRole)
                    model.setData(clicked, not model.data(clicked))
                self.signalMouseClick.emit([self.recorder])

                # Notify 'delegate' to update the whole line
                start = model.index(clicked.row(), 0)
                stop  = model.index(clicked.row(), columns)
                model.dataChanged.emit(start, stop)

            # Qt.ControlModifier
            elif self.modifier == Qt.ControlModifier:
                # first click with 'ctrl', set 'previous' to current line
                if self.recorder < 0:
                    self.recorder = clicked.row()

                # Add line clicked before 'control' pressed
                if self.recorder not in self.chosen:
                    self.chosen.append(self.recorder)

                # Add current clicked line if not added already
                if clicked.row() not in self.chosen:
                    self.chosen.append(clicked.row())
                self.signalMouseClick.emit(self.chosen)

                # Notify 'delegate' to update the whole line
                start = min(self.chosen)
                stop = max(self.chosen)
                indexFrom = model.index(start, 0)
                indexTo   = model.index(stop, columns)
                model.dataChanged.emit(indexFrom, indexTo)

            # Qt.ShiftModifier
            elif self.modifier == Qt.ShiftModifier:
                # first click with 'shift', set 'previous' to current line
                if self.recorder < 0:
                    self.recorder = clicked.row()

                # If there are lines selected by 'ctrl', get the last line
                lowest = 0
                highest = 0
                if self.chosen:
                    self.recorder = self.chosen[-1]
                    lowest = min(self.chosen)
                    highest = max(self.chosen)
                    self.chosen.clear()

                larger = max(self.recorder, clicked.row())
                smaller = min(self.recorder, clicked.row())
                for line in range(smaller, larger+1):
                    if line not in self.chosen:
                        self.chosen.append(line)
                self.signalMouseClick.emit(self.chosen)

                # Notify 'delegate' to update
                start = min(lowest, smaller)
                stop  = max(highest, larger)
                indexFrom = model.index(start, 0)
                indexTo   = model.index(stop, columns)
                model.dataChanged.emit(indexFrom, indexTo)

            # Qt.AltModifier
#           elif self.modifier == Qt.AltModifier:

            # Qt.MetaModifier
#           elif self.modifer == Qt.MetaModifier:
        super().mousePressEvent(event)


    def table_column_resize(self):
#       self.setColumnWidth(0, 100)
        self.setColumnWidth(4, 180)

    def table_view_update(self, data):
        if not data:
            return
        self.tabledata.table_model_data_update(data)
        self.table_column_resize()

    def table_view_renew(self, data):
        if not data:
            return
        self.tabledata.table_model_data_renew(data)
        self.table_column_resize()

    def table_view_reset(self):
        self.tabledata.table_model_data_reset()
        self.delegate.clearConditions()



#####################################################
class CustomTableModel(QAbstractTableModel):

    def __init__(self, data=[]):
        super().__init__()
        self._data = data
        self.headers = [
            'Serial',  'Selected', 'SourceKind', 'SourceAddr',
            'Param1',  'Param2',   'Param3',     'Param4',
            'Param5',  'Param6',   'Param7',     'Param8',
            'fzeval',  'exeval',   'fzsqrs',     'exsqrs',
        ]

    def rowCount(self, parent=QModelIndex()):
        if not self._data:
            return 0
        return len(self._data)

    def columnCount(self, parent=QModelIndex()):
        if not self._data:
            return 0
        if not self._data[0]:
            return 0
        return len(self._data[0])

    def headerData(self, section, orient, role):
        if section < 0 or section >= len(self.headers):
            return None
        return self.headers[section]

    def data(self, index, role=Qt.DisplayRole):
        row = index.row()
        col = index.column()

        if role == Qt.DisplayRole:
            if isinstance(self._data[row][col], float):
                return '{:.5f}'.format(self._data[row][col])
            elif col == 2:  # the column of source kind
                return 'source kind'
            elif col == 3:  # the column of source addr
                return os.path.basename(self._data[row][col])
            else:
                return str(self._data[row][col])
        elif role == Qt.CheckStateRole:
            if self._data[row][1]:
                return Qt.Checked
            else:
                return Qt.Unchecked
        elif role == Qt.TextAlignmentRole:
            return Qt.AlignVCenter
        else:
            return None

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        if role == Qt.EditRole and index.column == 1:
            self._data[index.row()][1] = value
            self.dataChanged.emit(index, index)
        return True


    def appendData(self, data):
        if not data:
            return

        currsize = len(self._data)
        datasize = len(data)
        self.beginInsertRows(QModelIndex(), currsize, currsize+datasize-1)
        for item in data:
            self._data.append(item)
        self.endInsertRows()

    def insertData(self, pos, data):
        if not data:
            return
        if pos < 0 or pos >= len(self._data):
            self.appendData(data)
            return

        nrows = len(data)
        self.beginInsertRows(QModelIndex(), pos, pos+nrows-1)
        for i in range(nrows):
            self._data.insert(pos+i, data[i])
        self.endInsertRows()

    def removeData(self, pos, nrows):
        # 1. Check for parameters
        if nrows <= 0:
            return
        if pos < 0 or pos >= len(self._data):
            return
        if pos+nrows > len(self._data):
            nrows = len(self._data)-pos

        # 2. Now, do the remove
        self.beginRemoveRows(QModelIndex(), pos, pos+nrows-1)
        for i in range(nrows):
            del self._data[pos+nrows-1-i]
        self.endRemoveRows()

    def renewData(self, data):
        self.beginResetModel()
        self._data.clear()
        self._data.extend(data)
        self.endResetModel()
    
    def resetData(self):
        self.beginResetModel()
        self._data.clear()
        self.endResetModel()

    def updateData(self, data):
        if not data:
            return
        purified = []
        for item in data:
            if item not in self._data:
                purified.append(item)
        self.appendData(purified)


class CustomTableDelegate(QStyledItemDelegate):
    def __init__(self):
        super().__init__()

    def paint(self, painter, option, index):
        painter.save()
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing)
        painter.setPen(QPen(Qt.black, 2))

#       painter.drawRect(option.rect.x(), option.rect.y(), option.rect.width(), option.rect.height())

        if index.column() == 1:
            w = option.rect.width()
            h = option.rect.height()
            l = option.rect.height() / 4
            bx = option.rect.x()
            by = option.rect.y()
            ox = bx + 0.5*w - 0.5*l
            oy = by + 0.5*h - 0.5*l
            pt1 = QPoint(ox, oy+l/3)
            pt2 = QPoint(ox+l/3, oy+l)
            pt3 = QPoint(ox+l, oy)
            # draw the rectangle of checkbox
            painter.drawRect(bx+0.5*w-0.25*h, by+0.25*h, 0.5*h, 0.5*h)
            data = index.data(Qt.CheckStateRole)
            if data == Qt.Checked:
                # draw the check symbol
                painter.drawPolyline([pt1, pt2, pt3])
        else:
            value = index.data(Qt.DisplayRole)
            painter.drawText(option.rect, Qt.AlignCenter, value)

        painter.restore()



#data1 = [
#    [1, '2411001', True, 0, '/home/clrc/excel', 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0, 4.0, 5.0],
#    [2, '2411002', True, 0, '/home/clrc/excel', 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 1.8, 2.0, 3.0, 4.0, 5.0],
#    [3, '2411003', True, 0, '/home/clrc/excel', 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 1.8, 2.0, 3.0, 4.0, 5.0],
#    [4, '2411004', True, 0, '/home/clrc/excel', 4.0, 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 1.8, 2.0, 3.0, 4.0, 5.0],
#    [5, '2411005', True, 0, '/home/clrc/xxecl', 5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 1.8, 2.0, 3.0, 4.0, 5.0],
#    [6, '2411005', True, 0, '/home/clrc/xxecl', 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.8, 6.0, 6.0, 6.0, 6.0],
#    [7, '2411005', True, 0, '/home/clrc/xxecl', 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.8, 7.0, 7.0, 7.0, 7.0],
#    [8, '2411005', True, 0, '/home/clrc/xxecl', 8.0, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.8, 8.0, 8.0, 8.0, 8.0],
#    [9, '2411005', True, 0, '/home/clrc/xxecl', 9.0, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.8, 9.0, 9.0, 9.0, 9.0],
#]
#app = QApplication()
#win = CustomCounterTableView()
#win.tablerenew(data1)
#win.resize(1000, 650)
#win.show()
#app.exec()

